solidity 区块链编程入门

solidity 区块链编程入门

编译器

Remix编译器 可以在线使用或离线使用.
使用 node 可以安装 solidity 编译器 solcjs
mac 可以通过homebrew 安装 编译器 solidity

源文件结构

pragma solidity ^0.5.2; 表示版本号及编辑器版本
import * as symbolName from "filename";导入其他源文件

值类型

  1. 整数类型分为 int/uint 定义. 可以显式设置占用空间大小,默认是 int256/uint256.

  2. 定长浮点类型: fixed/ufixed 表示各种大小有符号和无符号的定长浮点型.分别是 fixed128x19 和 ufixed128x19 的别名,第一个数字表示占用的位数,必须是 8 的倍数,第二个数字是可用的小数点位.

  3. 布尔型bool 分为 true 和 false

  4. 运算符与 javascript 相同,除 === 之外.

  5. 以太坊地址为 160 位即 20 字节大小.用 address 表示地址类型.地址有两种 address payable 可以接受以太币,而 address 则不行.前者可以隐式转换为普通地址,但普通地址要想转换为 payable 必须通过 payable()函数.

    1
    2
    3
    4
    5
    address public owner; // 定义地址
    owner.balance; //查看余额
    addressA.transfer(1 ether)// 像 A 转 1 eth,地址无效或余额不足会抛出异常.
    addressA.transfer.gas(120000)(1 ether) // 转账 附带 gas 的写法
    owner.send(1 ether) // send 是 transfer 的低级版本,有风险,合约失败返回 false,建议使用 transfer
  6. 每一个 contract 合约都有自己的类型,可以显式的转换为 adress 类型,只有当合约具有 receive(接收) 函数或 payable 回退函数时,才能显式和 address payable 类型相互转换.转换仍然使用 address()执行,如果没有接收函数和回退函数需要用 payable(address(x))转换为 address payable.
    对于合约可以使用 type(xx) 来获取合约的类型信息.

  1. 固定长字节数组,以 bytes 加数字表示,如 bytes2 表示 两个字节长度的数组,数组范围为 1-32.默认是 1.
    动态长度字节数组分两种,bytes 和 string(不支持索引访问)

引用类型

  1. 如果使用引用类型,必须明确数据存储在哪个位置.
    变量的储存位置有三种,memory 修饰的变量储存在内存中仅在函数运行期有效不能外部调用, storage 修饰的变量存储在区块链上只有合约存在就有效,calldata 指调用数据,用来保存函数参数,是一个只读位置.
    函数返回值默认是 memory,函数局部变量的默认数据是 storage,状态变量的默认数据是 storage.

  2. 数组截图在声明时指定长度,也可以动态调整.push()添加一个元素,返回对它的引用. 同理还有 pop 函数.
    bytes 和 string 也是数组.string 不能使用索引, bytes 等同于 byte[] 但 gas 消耗更低.可以使用 new 关键字创建内存数组,但不能改变其内存数组的大小.
    solidity 提供数组切片 x[start:end],仅仅可用于 calldata

    1
    2
    3
    4
    5
    6
    7
    uint[][5] x = [1,2,3,4,5];
    x[0] = 6; // x 为[6,2,3,4,5]
    x.length = 5;
    uint[] y = [1,2] // 动态长度
    xxxtype[] public xxxx; // 自定义 xx 类型数组
    bytes memory b = new bytes(9)
    uint[3][5] x; //与大多数编程语言相反,为 5 行 3 列.
    1. 结构体是自定义数据类型,可以是字符串整型等基础类型,也可以是数组映射结构体等复杂类型.可以使用关键字 struct 定义.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      //定义
      struct Bank{
      address owner;
      uint balance;
      }
      // 初始化方法一
      Bank b = Bank({
      owner: msg.sender,
      balance: 5
      })
      // 方法 2
      Bank c = Bank(msg.sender, 7)
      // 重新赋值
      c.balance = 1;
      delete b;//重置 b 的所有值为 0,除了 mapping 类型.
  3. 映射/字典 定义方式为 mapping, key 值最好是基础类型.Solidity 没有提供其迭代的方法.

    1
    2
    3
    4
    mapping(string => uint) public balances; // public 会自动创建一个 getter 函数.
    balances['charles'] = 1;
    balances['ada']; // 没有设置 key 的返回 0
    delete balances["John"]; // delete 不会删除元素,只会重置其初始值.

    delete 用来初始化类型的值,对映射无效.

  4. 枚举可用来创建一定数量的”常量值”够成的自定义数据类型,可以显式转为整型,但不能隐式转换.一般当做状态机使用.长度不能超过 256 位.

    1
    2
    3
    4
    5
    6
    7
    8
    // 定义状态机
    enum State {Created, Locked,Inactive};
    // 声明 state 变量
    State public state;
    // 赋值
    state = State.Created;
    // 显式转换
    uint createdState = uint(State.Created);
  5. 类型转换和类型推断
    隐式转换: int和 int,uint 和 uint 可以相互转换,但 int 和 uint 不能转换,整数类型可以转换为 bytes,但反过来不行,任何可以转换为 uint160 的变量都可以转换为 address 地址类型.
    显示转换: uint8(a)
    类型推断: var 会在第一次赋值时推断变量类型,不可以用于函数参数,使用时小心,有时候会推断出错误类型.

单位和全局变量

  1. 货币单位
    wei, gwei, ether,默认后缀是 wei. 1ether = 10 的 18 次方 wei.

    1
    1 ether = 10 ** 18 wei;
  2. 时间单位
    seconds,minutes,hours,days,weeks 都可作为后缀,默认以 seconds 为单位.(years 因为闰年的原因已去除).
    这些单位不能直接用在变量后面,要用变量 乘 1seconds/其他单位 来使用.

    1
    uint a = 1 * 1 days; // 值为 86400 秒
  3. 全局变量
    solidity 提供的通用函数或变量.

  • block 区块信息
  • msg 消息信息
  • tx 交易信息
  • abi 编码及解码函数
  • 错误处理 throw 抛出异常,require检查由输入和外部引起的错误,assert检查内部错误,revert终止运行回撤状态并提供一个解释性字符串
  • 数学密码学函数 addmod,mulmod,keccak256,sha256,repemd160,ecrecover
  • address 地址成员,包含 balance 余额,code 代码,transfer,send,call,delegatecall,staticcall
  • 合约相关: this 表示当前合约,selfdestruct 销毁合约.
  • 类型信息: type(x) 检索类型信息.属性包含 name,runtimeCode 等等…

表达式和控制语句

Solidity 支持 js 中大部分语句 if,else,while,do,for,break,continue,return, 三元表达式,不支持 switch 和 goto 语句.tryCatch语句只能用于外部函数调用和合约创建调用.
Solidity 没有 js 中的非 boolean 类型自动转换的特性.
使用循环时注意 gas 的数量,防止合约失败.在合约中优先使用循环而不是递归,EVM 的最大调用栈的深度是 1024.
solidity 内部允许使用元祖(tuple)类型.

1
2
3
4
5
6
7
8
function g() public {
//基于返回的元组来声明变量并赋值
(uint x, bool b, uint y) = f();
//交换两个值的通用窍门——但不适用于非值类型的存储 (storage) 变量。
(x, y) = (y, x);
//元组的末尾元素可以省略(这也适用于变量声明)。
(index,,) = f(); // 设置 index 为 7
}

solidity 作用域规则可以参考 javascript.

合约

合约类似于编程语言中的类,可以通过 new 关键字来创建一个新合约,在合约可以调用另一个合约的方法.调用另一个合约时会很自信一个 EVM 函数调用,这会切换执行时的上下文,这样前一个合约的状态变量就不能访问了.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract infoFeed{
...
}
contract Consumer{
InfoFeed feed; // 指向一个已经部署的合约;
function setFeed(address addr){
// 传入部署合约的区块链地址
feed = Infofeed(addr) // 显示进行类型转换,不会调用构造函数
}
// 创建合约实例
function createNewFeed(){
feed = new InfoFeed() // 调用构造函数
}
}

编译器自动为所有 public 状态的变量创建 getter 函数.外部访问时被认作一个函数.
状态变量声明为 constant (常量)或者 immutable (不可变量)
合约之外的函数(也称为“自由函数”)始终具有隐式的 internal 可见性。 它们的代码包含在所有调用它们合约中,类似于内部库函数。

函数

函数也是一种值类型,可以将函数传递给另外一个函数作为参数,可以再函数中返回一个函数.
1
2
3
4
5
6
// 定义 ,可以由多个返回值
function increment(uint x) returns (uint a, uint b) {
a = x+1;
b = a*2;
return (a,b);
}
函数类型分为两类: 内部 internal 函数类型和外部 external 函数类型.如果函数不需要返回,可以省略 returns xx.一个函数默认是内部函数.
函数有四种可见性,public(公开),private(私有,定义的合约内部访问),external(不能再合约内部调用),internal(只能从内部访问).在public函数中,solidity会立刻把函数数组参数拷贝到内存中,而external函数可以直接从calldata中读取数据。内存分配是昂贵的,直接从calldata中读取是便宜的.
constructor 是构造函数,在创建合约时执行,并在内部初始化 代码 和状态变量.构造函数运行后将合约最终代码部署到区块链.
View 视图函数: 减函数声明为 view 类型,这种情况下要保证不修改状态(包括修改状态,产生事件,创建合约,发送 eth,调用任何没有标记 view 和 pure 的函数,销毁合约等). Constant 之前是 view 的别名,0.5.0 移除.
Pure 纯函数: 承诺不读取也不修改状态.访问 address 和 block 等其他信息都属于读取状态.纯函数能适应 revert()和 require()在发生错误是还原状态.
一个合约最多有一个接收函数 receive(),声明为`receive() external payable {...}`不需要 function 关键字,也没有参数和返回值,必须用 external 和 payable 修饰.如果它不存在就会调用有 payable 的 fallback 回退函数.如果两个都没有就会在交易时抛出异常.
函数修饰符: modifier(修改器) 用于在函数执行前检查某种前置条件是否满足
1
2
3
4
modifier onlyOwner{
require(msg.sender == owner); // 判断调用合约的是不是合约所有者
_;// 下划线表示私有修改符的函数的方法体的替换位置
}
回退函数 fallback: 每个合约最多只有一个,这个函数无参数也无返回值.一般有两种情况对调用回退函数,一是调用合约时没有匹配到任何函数,二是给合约发送 eth 时,交易中没有附带任何其他数据,也会调用回退函数.新版本不再推荐,推荐使用 receive 函数.
1
2
3
4
5
contract Test{
fallback() external payable{
throw; // 执行失败返回 eth 给发送者. payable 修饰符用来接收 eth
}
}
自毁函数: selfdestruct(address)用来摧毁合约并将 eth 转移到给定地址.当你发现合约有问题不想让其他人使用时就可以摧毁这个合约了.摧毁之后再有人发送eth 到这个地址就会消失.
solidity 支持函数重载和函数重写 overriding.父合约标记为 virtual 函数可以再继承合约里重写.重写的函数要用 override 修饰.

继承: solidity 合约可以通过 is 关键字实现从父合约中继承.
1
2
3
4
5
6
7
8
9
10
contract A{

}
contract B is A{

}
// 多重继承
contract C is A,B{

}
接口:接口 interface 是 solidity 在版本 0.4.11 版本后引入的,接口所有函数都是抽象函数, 关键字 abstract定义抽象函数.
合约中有的函数没有函数体只有函数定义的是抽象合约.

库:库是一中不同类型的合约,没有存储,不拥有 eth.库中的代码可以被其他合约调用而不需要重新部署,这样可以节省大量 gas.库中没有可支付的函数(payable),没有 fallback 回退函数,
库的调用通过 DELEGATECALL(委托调用,除此之外好友 call,staticcall都是低级的函数,破坏了 solidity 的类型安全性,谨慎使用) 实现,不切换上下文.

Using for:using for 的声明方式是 using lib for a,意为库 lib 中所有函数默认接收 a 实例作为第一个参数.`using Balances for *`引入库 Balances 中的函数被附加在任意的类型上。
1
2
3
4
5
6
7
8
9
10
library C{
funtion a() returns (address){
return this;
}
}
contract A{
function test() returns(address){
return C.a(); // 返回 A 合约的地址
}
}

事件

真实环境中我们需要发送交易(Transaction)来调用智能合约,我们无法立即获得合约的返回值,此时调用返回值只是该交易的 txid 或 tx hash 值.当事件真正发生时,合约将事件写入区块链时,前端才能进行响应.
1
2
3
4
5
6
7
8
9
10
	//定义事件
event Sent(address,indexed from...);

// 触发事件
emit Sent(address,...)

// js 调用事件
var ClientReceipt = web3.eth.contract(xx);
var event = ClientReceipt.Sent();
event.watch(function(error,result){...})